﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Urb.Weather.Core;
using Urb.Weather.Core.Engine;
using System.Threading;
using System.Net;

namespace Urb.Weather.Engine
{
    public class Engine : IEngine
    {
        private EngineState state = EngineState.Stopped;
        private Thread engineThread;
        private Timer periodicWorkItemQueueTimer;

        private object mainLock = new object();
        private object runningLock = new object();
        private object immediateWorkItemQueueLock = new object();
        private object periodicWorkItemQueueLock = new object();

        private AutoResetEvent somethingHappenedEvent = new AutoResetEvent(false);

        private bool stopRequested = false;

        private WorkItemRegistrationTokenGenerator tokenGenerator = new WorkItemRegistrationTokenGenerator();

        private Queue<IWorkItem> immediateWorkItemQueue = new Queue<IWorkItem>();
        private LinkedList<PeriodicWorkItem> periodicWorkItemQueue = new LinkedList<PeriodicWorkItem>();
        
        public event EngineStateChangedEventHandler StateChanged;

        public Engine()
        {
            periodicWorkItemQueueTimer = new Timer(PeriodicWorkItemQueueTimerTick);
        }

        public EngineState State
        {
            get { return state; }
        }

        public void Queue(IWorkItem workItem)
        {
            lock (immediateWorkItemQueueLock)
            {
                immediateWorkItemQueue.Enqueue(workItem);
            }

            somethingHappenedEvent.Set();
        }

        public void Queue(IDownloadStringWorkItem workItem)
        {
            Queue(
                new WorkItem<IDownloadStringWorkItem>()
                {
                    Action = ExecuteDownloadStringWorkItem,
                    Parameter1 = workItem
                }
                );
        }

        public IWorkItemRegistrationToken Queue(IWorkItem workItem, IPeriod period, TimeSpan initialDue)
        {
            DateTime nextOccurrence = DateTime.Now.Add(initialDue);
            IWorkItemRegistrationToken token = tokenGenerator.Next();
            
            lock (periodicWorkItemQueueLock)
            {
                periodicWorkItemQueue.AddLast(
                    new PeriodicWorkItem()
                    {
                        Token = token,
                        NextOccurrence = nextOccurrence,
                        Period = period,
                        WorkItem = workItem
                    }
                );

                SetupPeriodicWorkItemTimer();
            }

            return token;
        }

        private void SetupPeriodicWorkItemTimer()
        {
            lock (mainLock)
            {
                if (state == EngineState.Stopped)
                    return;
            }

            lock (periodicWorkItemQueueLock)
            {
                if (periodicWorkItemQueue.Count <= 0)
                {
                    periodicWorkItemQueueTimer.Change(Timeout.Infinite, Timeout.Infinite);                        
                    return;
                }

                DateTime firstOccurrence = periodicWorkItemQueue.Min(wi => wi.NextOccurrence);
                DateTime now = DateTime.Now;

                if (firstOccurrence > now)
                {
                    TimeSpan timespan = firstOccurrence - now;

                    periodicWorkItemQueueTimer.Change((int)timespan.TotalMilliseconds, Timeout.Infinite);
                }
                else
                {
                    somethingHappenedEvent.Set();
                }
            }
        }

        public IWorkItemRegistrationToken Queue(IDownloadStringWorkItem workItem, IPeriod period, TimeSpan initialDue)
        {
            return Queue(
                new WorkItem<IDownloadStringWorkItem>()
                {
                    Action = ExecuteDownloadStringWorkItem,
                    Parameter1 = workItem
                },
                period,
                initialDue
                );
        }

        public void Dequeue(IWorkItemRegistrationToken token)
        {
            throw new NotImplementedException();
        }

        public void Start()
        {
            lock (mainLock)
            {
                if (state != EngineState.Stopped)
                    return;

                lock (runningLock)
                {
                    if (engineThread != null)
                    {
                        engineThread.Join();
                        engineThread = null;
                    }

                    engineThread = new Thread(MainEngineMethod);
                    engineThread.Priority = ThreadPriority.Normal;

                    somethingHappenedEvent.Reset();
                                 
                    engineThread.Start();

                    RaiseStateChanged(EngineState.Started);

                    somethingHappenedEvent.Set();
                }
            }
        }

        public void RequestStop()
        {
            lock (mainLock)
            {
                if (state != EngineState.Started)
                    return;
                
                RaiseStateChanged(EngineState.Stopping);

                stopRequested = true;

                somethingHappenedEvent.Set();
            }
        }

        public void Stop()
        {
            lock (mainLock)
            {
                if (state == EngineState.Stopped)
                    return;
                
                RequestStop();

                periodicWorkItemQueueTimer.Change(Timeout.Infinite, Timeout.Infinite);

                somethingHappenedEvent.Set();

                engineThread.Join();
            }
        }

        private void MainEngineMethod()
        {
            lock (runningLock)
            {
                while (true)
                {
                    somethingHappenedEvent.WaitOne();

                    if (stopRequested)
                    {
                        RaiseStateChanged(EngineState.Stopped);
                        stopRequested = false;
                        return;
                    }

                    while (IsWorkItemPending())
                    {
                        IWorkItem workItem = DequeueWorkItem();

                        if (workItem == null)
                            continue;

                        workItem.Execute(this);

                        if (stopRequested)
                        {
                            RaiseStateChanged(EngineState.Stopped);
                            stopRequested = false;
                            return;
                        }
                    }
                }
            }
        }

        private bool IsWorkItemPending()
        {
            lock (immediateWorkItemQueueLock)
            {
                if (immediateWorkItemQueue.Count > 0)
                    return true;
            }

            lock (periodicWorkItemQueueLock)
            {
                DateTime now = DateTime.Now;
                if (periodicWorkItemQueue.Any(wi => wi.NextOccurrence <= now))
                    return true;
            }

            return false;
        }

        private void RaiseStateChanged(EngineState engineState)
        {
            this.state = engineState;

            if (StateChanged != null)
                StateChanged(this, engineState);
        }

        private IWorkItem DequeueWorkItem()
        {
            lock (immediateWorkItemQueueLock)
            {
                if (immediateWorkItemQueue.Count > 0)
                    return immediateWorkItemQueue.Dequeue();
            }

            lock (periodicWorkItemQueueLock)
            {
                if (periodicWorkItemQueue.Count > 0)
                {
                    DateTime now = DateTime.Now;

                    var periodicWorkItem =
                        periodicWorkItemQueue.OrderBy(wi => wi.NextOccurrence).FirstOrDefault(wi => wi.NextOccurrence <= now);

                    if (periodicWorkItem != null)
                    {
                        periodicWorkItem.NextOccurrence = periodicWorkItem.Period.GetNextOccurrence(now);

                        SetupPeriodicWorkItemTimer();

                        return periodicWorkItem.WorkItem;
                    }
                }
            }

            return null;
        }

        private void ExecuteDownloadStringWorkItem(IDownloadStringWorkItem workItem)
        {
            var webClient = new WebClient();
            webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(DownloadStringCompleted);
            webClient.DownloadStringAsync(new Uri(workItem.Uri), workItem);
        }

        private void DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            if (sender == null)
                throw new ArgumentNullException("sender");

            var senderAsWebClient = sender as WebClient;

            if (senderAsWebClient == null)
                throw new ArgumentException("Wrong sender type", "sender");

            try
            {
                if (e.UserState != null && e.UserState is IDownloadStringWorkItem)
                {
                    IDownloadStringWorkItem workItem = (IDownloadStringWorkItem)e.UserState;

                    if (e.Cancelled)
                    {
                        // TODO
                    }
                    else if (e.Error != null)
                    {
                        Queue(
                            new WorkItem<Exception>()
                            {
                                Action = workItem.OnError,
                                Parameter1 = e.Error
                            }
                            );
                    }
                    else
                    {
                        Queue(
                            new WorkItem<string>()
                            {
                                Action = workItem.OnSuccess,
                                Parameter1 = e.Result
                            }
                        );
                    }
                }
            }
            finally
            {
                senderAsWebClient.DownloadStringCompleted -= DownloadStringCompleted;
                senderAsWebClient.Dispose();
            }
        }
        
        private void PeriodicWorkItemQueueTimerTick(object state)
        {
            somethingHappenedEvent.Set();
        }
    }
}
